Skip to content

feat: add timeline tab to package page#2245

Open
43081j wants to merge 26 commits intomainfrom
jg/going-back-in-time
Open

feat: add timeline tab to package page#2245
43081j wants to merge 26 commits intomainfrom
jg/going-back-in-time

Conversation

@43081j
Copy link
Copy Markdown
Contributor

@43081j 43081j commented Mar 23, 2026

🔗 Linked issue

#2244

🧭 Context

A timeline will help show how a package has changed over time in good and bad ways.

📚 Description

This is basically a timeline which shows the published versions of a package.

Alongside each version, it shows significant changes.

Positive changes flagged:

  • ESM
  • Dependency count decrease
  • Install size decrease
  • Trusted publishing enabled
  • Provenance enabled

Negatives flagged:

  • CJS
  • Dependency count increase
  • Install size increase
  • Trusted publishing disabled
  • Provenance disabled

Neutrals flagged:

  • License changes

@vercel
Copy link
Copy Markdown

vercel bot commented Mar 23, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
npmx.dev Ready Ready Preview, Comment Apr 7, 2026 10:35pm
2 Skipped Deployments
Project Deployment Actions Updated (UTC)
docs.npmx.dev Ignored Ignored Preview Apr 7, 2026 10:35pm
npmx-lunaria Ignored Ignored Apr 7, 2026 10:35pm

Request Review

@github-actions
Copy link
Copy Markdown

github-actions bot commented Mar 23, 2026

Lunaria Status Overview

🌕 This pull request will trigger status changes.

Learn more

By default, every PR changing files present in the Lunaria configuration's files property will be considered and trigger status changes accordingly.

You can change this by adding one of the keywords present in the ignoreKeywords property in your Lunaria configuration file in the PR's title (ignoring all files) or by including a tracker directive in the merged commit's description.

Tracked Files

File Note
i18n/locales/en.json Source changed, localizations will be marked as outdated.
Warnings reference
Icon Description
🔄️ The source for this localization has been updated since the creation of this pull request, make sure all changes in the source have been applied.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 23, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a package timeline feature: a new page at /package-timeline/:org?/:packageName/v/:version with SSR-cached API pagination and client-side "load more", per-version computed sub-events (install size, dependency deltas, license, ESM/Typescript/trusted-publisher/provenance changes), best-effort per-version install-size fetches and caching, and UI including PackageHeader timeline tab and keyboard shortcut. Normalises license/type fields in packument handling, extends SlimVersion, adds i18n keys/schema for timeline and shortcut, and includes unit tests for the timeline API endpoint.

Possibly related issues

Possibly related PRs

Suggested labels

needs review

Suggested reviewers

  • shuuji3
  • graphieros
🚥 Pre-merge checks | ✅ 1
✅ Passed checks (1 passed)
Check name Status Explanation
Description check ✅ Passed The pull request description accurately describes the timeline feature implementation, including the specific change categories (positive, negative, neutral) and package modifications being tracked.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch jg/going-back-in-time

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov
Copy link
Copy Markdown

codecov bot commented Mar 23, 2026

Codecov Report

❌ Patch coverage is 81.25000% with 3 lines in your changes missing coverage. Please review.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
app/components/Package/Header.vue 77.77% 2 Missing ⚠️
app/composables/npm/usePackage.ts 80.00% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@ghostdevv ghostdevv linked an issue Mar 23, 2026 that may be closed by this pull request
3 tasks
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 7fe43926-024b-4e21-9ef6-4a2a338bd6b7

📥 Commits

Reviewing files that changed from the base of the PR and between e3b575a and 24e9bad.

📒 Files selected for processing (7)
  • app/components/Package/Header.vue
  • app/composables/npm/usePackage.ts
  • app/pages/package-timeline/[[org]]/[packageName].vue
  • i18n/locales/en.json
  • i18n/schema.json
  • server/api/registry/timeline/[...pkg].get.ts
  • shared/types/npm-registry.ts

Comment on lines +136 to +145
const versionSubEvents = computed(() => {
const result = new Map<string, SubEvent[]>()
const entries = timelineEntries.value

// Sort by semver to find each version's true predecessor
const semverSorted = [...entries].sort((a, b) => compare(b.version, a.version))
const prevBySemver = new Map<string, TimelineVersion>()
for (let i = 0; i < semverSorted.length - 1; i++) {
prevBySemver.set(semverSorted[i]!.version, semverSorted[i + 1]!)
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Don't infer the semver predecessor from a paged slice.

timelineEntries only contains the current publish-time page. Sorting that slice by semver and taking semverSorted[i + 1] does not guarantee the true predecessor, so maintenance releases on older branches can end up compared against unrelated versions and produce false sub-events. This needs predecessor data from the full version set, or the comparison should be suppressed until the real predecessor is loaded.

@MatteoGabriele
Copy link
Copy Markdown
Member

Super cool feature! I took a quick look at it, and there are a couple of UI/UX aspects that aren't very clear

What happens when you click on each version?

I know the "diff" link appears and lets you check differences between versions, but it’s not obvious that clicking the version itself opens that feature. Maybe we can improve discoverability and the link's purpose?

Re-rendering

Every time I click a version, it does all the requests again (cached), but it seems to be causing a lot of repainting

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
test/unit/server/api/registry/timeline/pkg.get.spec.ts (2)

7-7: Avoid Function in the cached-handler stub.

Using Function removes parameter/return checks and can hide handler-signature drift; prefer an explicit typed callback signature.

♻️ Proposed refactor
-vi.stubGlobal('defineCachedEventHandler', (fn: Function) => fn)
+vi.stubGlobal(
+  'defineCachedEventHandler',
+  <T>(fn: (event: H3Event) => T, _opts?: unknown) => fn,
+)

As per coding guidelines "Ensure you write strictly type-safe code, for example by ensuring you always check when accessing an array value by index".


189-190: Remove any from version lookup predicates.

The predicate type can be inferred from result.versions; any weakens test type guarantees.

♻️ Proposed refactor
-    const latest = result.versions.find((v: any) => v.version === '1.0.0')
-    const next = result.versions.find((v: any) => v.version === '2.0.0-beta.1')
+    const latest = result.versions.find(v => v.version === '1.0.0')
+    const next = result.versions.find(v => v.version === '2.0.0-beta.1')

As per coding guidelines "Ensure you write strictly type-safe code, for example by ensuring you always check when accessing an array value by index".


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 73024f04-51f6-4e72-aa61-63bf62b35d6a

📥 Commits

Reviewing files that changed from the base of the PR and between 24e9bad and dc1ee22.

📒 Files selected for processing (1)
  • test/unit/server/api/registry/timeline/pkg.get.spec.ts

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
app/pages/package-timeline/[[org]]/[packageName].vue (1)

390-392: ⚠️ Potential issue | 🟡 Minor

Empty state does not distinguish between loading and initial fetch failure.

If the initial useAsyncData request fails, timelineEntries remains empty and the spinner displays indefinitely. Consider adding an error state to inform users when the initial load has failed.

Suggested approach

Capture the error from useAsyncData:

-const { data: initialTimeline } = await useAsyncData(`timeline:${packageName.value}`, () =>
+const { data: initialTimeline, error: initialError } = await useAsyncData(`timeline:${packageName.value}`, () =>
   fetchTimeline(0),
 )

Then render an error state in the template:

       <!-- Empty state -->
-      <div v-else-if="!timelineEntries.length" class="py-20 text-center">
+      <div v-else-if="initialError" class="py-20 text-center text-red-600 dark:text-red-400">
+        {{ $t('package.timeline.initial_load_error') }}
+      </div>
+      <div v-else-if="!timelineEntries.length" class="py-20 text-center">
         <span class="i-svg-spinners:ring-resize w-5 h-5 text-fg-subtle" />
       </div>
🧹 Nitpick comments (2)
app/pages/package-timeline/[[org]]/[packageName].vue (2)

14-14: Prefer auto-imported $t() over destructuring from useI18n().

The project pattern uses the globally auto-imported $t() in <script setup> rather than destructuring t from useI18n(). The $t() global works inside computed properties and callbacks in this Nuxt i18n setup.

Suggested change
-const { t } = useI18n()

Then replace all t(...) calls with $t(...) in the computed property (lines 175, 179, 193, 194, 207, 219, 226, 236, 243, 253, 260, 270, 277).

Based on learnings: "In this Nuxt 4 project with nuxtjs/i18n v10, $t() and other globals like $n, $d are exposed in <script setup> and work inside callbacks... Do not destructure t from useI18n(); rely on the global provided by Nuxt i18n in script setup."


74-74: Blocking loadMore while sizes are fetching may degrade UX.

sizesLoading.value prevents loading more versions whenever any install-size request is in flight. Since size fetches are best-effort and don't affect pagination correctness, this coupling unnecessarily delays content loading for users scrolling through the timeline.

Consider decoupling size loading from pagination:

Suggested change
 async function loadMore() {
-  if (loadingMore.value || sizesLoading.value) return
+  if (loadingMore.value) return

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 2dd7a2a9-64d2-4901-b5e6-487ee0837186

📥 Commits

Reviewing files that changed from the base of the PR and between dc1ee22 and 1972967.

📒 Files selected for processing (3)
  • app/components/AppFooter.vue
  • app/pages/package-timeline/[[org]]/[packageName].vue
  • i18n/locales/en.json
🚧 Files skipped from review as they are similar to previous changes (1)
  • i18n/locales/en.json

@43081j
Copy link
Copy Markdown
Contributor Author

43081j commented Mar 25, 2026

What if the version link took users directly to the package page? I worry that simply linking to the specific version while staying in the timeline may not clearly show what happened

agreed. it was this originally, i have changed it back.

I was also wondering if it might be a good idea to merge this page's functionalities with the existing Version history feature. They seem similar enough to be combined into a single page.

I'm also not sure. they have different purposes. it just so happens the only timeline entries right now are version specific, but there's nothing stopping us adding timeline events which are not associated with a version.

@43081j 43081j force-pushed the jg/going-back-in-time branch from 752bd93 to 58297e1 Compare April 7, 2026 21:47
Co-authored-by: Alex Savelyev <91429106+alexdln@users.noreply.github.com>
@alexdln
Copy link
Copy Markdown
Member

alexdln commented Apr 7, 2026

@43081j Is it possible to add a loader for the sizes? It took about 30 seconds for me (nuxt), and I thought there was nothing special in the package (and would have just gone to another page to look further keeping sure that nothing changed in terms of size)

@43081j
Copy link
Copy Markdown
Contributor Author

43081j commented Apr 7, 2026

im going to try refactor some things and will come back to that once i have 👍

@43081j
Copy link
Copy Markdown
Contributor Author

43081j commented Apr 7, 2026

ok calling it a night for now but basically i've reworked the size calculation to happen in a paged way just like the timeline itself does.

so for each page of timeline (25 items), we send do 2 requests:

  • timeline itself (which is SSR'd as far as i understand, nuxt stuff)
  • size calculations (always lazily loaded)

load more then triggers these two requests again.

in cases where we fail to compute the size, or for whatever reason we can't figure the dependency tree out, we skip those versions in the size response. so notices dont show with "changed by 100%!".

also adds a css loading bar at the top that shows when these requests are in flight

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add a timeline tab to packages

6 participants